package dedup

import (
	"crypto/sha256"
	"encoding/binary"
	"fmt"
	"hash"

	"github.com/zeebo/blake3"
)

// BlockFingerprinter provides block fingerprinting capabilities
type BlockFingerprinter interface {
	CalculateFingerprint(data []byte) string
	CalculateSimilarity(hash1, hash2 string) float64
	GetFingerprintType() string
}

// HashFingerprinter uses cryptographic hash functions for block fingerprinting
type HashFingerprinter struct {
	algorithm string
	hasher    hash.Hash
}

// NewHashFingerprinter creates a new hash-based fingerprinter
func NewHashFingerprinter(algorithm string) (*HashFingerprinter, error) {
	var hasher hash.Hash
	switch algorithm {
	case "sha256":
		hasher = sha256.New()
	case "blake3":
		hasher = blake3.New()
	default:
		return nil, fmt.Errorf("unsupported hash algorithm: %s", algorithm)
	}

	return &HashFingerprinter{
		algorithm: algorithm,
		hasher:    hasher,
	}, nil
}

// CalculateFingerprint calculates a cryptographic hash fingerprint
func (f *HashFingerprinter) CalculateFingerprint(data []byte) string {
	// Create a new hasher for each calculation to avoid race conditions
	var h hash.Hash
	switch f.algorithm {
	case "sha256":
		h = sha256.New()
	case "blake3":
		h = blake3.New()
	}

	h.Write(data)
	return fmt.Sprintf("%x", h.Sum(nil))
}

// CalculateSimilarity calculates similarity between two hash fingerprints
func (f *HashFingerprinter) CalculateSimilarity(hash1, hash2 string) float64 {
	if hash1 == hash2 {
		return 1.0
	}
	return 0.0 // Cryptographic hashes are either identical or completely different
}

// GetFingerprintType returns the type of fingerprint
func (f *HashFingerprinter) GetFingerprintType() string {
	return fmt.Sprintf("hash-%s", f.algorithm)
}

// SimHashFingerprinter implements SimHash for similarity detection
type SimHashFingerprinter struct {
	bits      int
	algorithm string
}

// NewSimHashFingerprinter creates a new SimHash fingerprinter
func NewSimHashFingerprinter(bits int, algorithm string) (*SimHashFingerprinter, error) {
	if bits <= 0 || bits > 256 {
		return nil, fmt.Errorf("invalid bit size: %d", bits)
	}

	return &SimHashFingerprinter{
		bits:      bits,
		algorithm: algorithm,
	}, nil
}

// CalculateFingerprint calculates a SimHash fingerprint
func (f *SimHashFingerprinter) CalculateFingerprint(data []byte) string {
	// Calculate hash of the data
	var hash []byte
	switch f.algorithm {
	case "sha256":
		h := sha256.Sum256(data)
		hash = h[:]
	case "blake3":
		h := blake3.Sum256(data)
		hash = h[:]
	default:
		h := sha256.Sum256(data)
		hash = h[:]
	}

	// Convert hash to SimHash
	simhash := f.calculateSimHash(hash)
	return fmt.Sprintf("%016x", simhash)
}

// calculateSimHash calculates SimHash from input hash
func (f *SimHashFingerprinter) calculateSimHash(hash []byte) uint64 {
	// Use the first 8 bytes of the hash for 64-bit SimHash
	if len(hash) >= 8 {
		return binary.BigEndian.Uint64(hash[:8])
	}

	// Pad if necessary
	var result uint64
	for i, b := range hash {
		result |= uint64(b) << (uint(i) * 8)
	}
	return result
}

// CalculateSimilarity calculates similarity between two SimHash fingerprints
func (f *SimHashFingerprinter) CalculateSimilarity(hash1, hash2 string) float64 {
	// Parse hex strings to integers
	var h1, h2 uint64
	fmt.Sscanf(hash1, "%016x", &h1)
	fmt.Sscanf(hash2, "%016x", &h2)

	// Calculate Hamming distance
	distance := f.hammingDistance(h1, h2)
	maxDistance := float64(f.bits)

	// Convert to similarity (0 = completely different, 1 = identical)
	similarity := 1.0 - (float64(distance) / maxDistance)
	return similarity
}

// hammingDistance calculates Hamming distance between two bit strings
func (f *SimHashFingerprinter) hammingDistance(x, y uint64) int {
	xor := x ^ y
	distance := 0
	for xor != 0 {
		distance += int(xor & 1)
		xor >>= 1
	}
	return distance
}

// GetFingerprintType returns the type of fingerprint
func (f *SimHashFingerprinter) GetFingerprintType() string {
	return fmt.Sprintf("simhash-%d-%s", f.bits, f.algorithm)
}

// MinHashFingerprinter implements MinHash for set similarity
type MinHashFingerprinter struct {
	numHashes int
	algorithm string
	seeds     []uint64
}

// NewMinHashFingerprinter creates a new MinHash fingerprinter
func NewMinHashFingerprinter(numHashes int, algorithm string) (*MinHashFingerprinter, error) {
	if numHashes <= 0 {
		return nil, fmt.Errorf("invalid number of hashes: %d", numHashes)
	}

	// Generate random seeds for hash functions
	seeds := make([]uint64, numHashes)
	for i := 0; i < numHashes; i++ {
		seeds[i] = uint64(i*1103515245 + 12345) // Simple pseudo-random
	}

	return &MinHashFingerprinter{
		numHashes: numHashes,
		algorithm: algorithm,
		seeds:     seeds,
	}, nil
}

// CalculateFingerprint calculates a MinHash fingerprint
func (f *MinHashFingerprinter) CalculateFingerprint(data []byte) string {
	// Calculate multiple hash values
	hashes := make([]uint64, f.numHashes)

	for i, seed := range f.seeds {
		hashes[i] = f.calculateMinHash(data, seed)
	}

	// Convert to string representation
	result := ""
	for _, h := range hashes {
		result += fmt.Sprintf("%016x", h)
	}
	return result
}

// calculateMinHash calculates a single MinHash value
func (f *MinHashFingerprinter) calculateMinHash(data []byte, seed uint64) uint64 {
	// Calculate hash of data with seed
	var hash uint64
	switch f.algorithm {
	case "sha256":
		h := sha256.Sum256(data)
		// XOR with seed and take first 8 bytes
		hashed := binary.BigEndian.Uint64(h[:8]) ^ seed
		hash = hashed
	case "blake3":
		h := blake3.Sum256(data)
		hashed := binary.BigEndian.Uint64(h[:8]) ^ seed
		hash = hashed
	default:
		h := sha256.Sum256(data)
		hashed := binary.BigEndian.Uint64(h[:8]) ^ seed
		hash = hashed
	}
	return hash
}

// CalculateSimilarity calculates similarity between two MinHash fingerprints
func (f *MinHashFingerprinter) CalculateSimilarity(hash1, hash2 string) float64 {
	if len(hash1) != len(hash2) || len(hash1) != f.numHashes*16 {
		return 0.0
	}

	// Parse hash strings
	h1 := f.parseMinHash(hash1)
	h2 := f.parseMinHash(hash2)

	// Calculate Jaccard similarity
	matches := 0
	for i := 0; i < f.numHashes; i++ {
		if h1[i] == h2[i] {
			matches++
		}
	}

	return float64(matches) / float64(f.numHashes)
}

// parseMinHash parses MinHash string to uint64 slice
func (f *MinHashFingerprinter) parseMinHash(hashStr string) []uint64 {
	hashes := make([]uint64, f.numHashes)
	for i := 0; i < f.numHashes; i++ {
		start := i * 16
		end := start + 16
		if end <= len(hashStr) {
			var h uint64
			fmt.Sscanf(hashStr[start:end], "%016x", &h)
			hashes[i] = h
		}
	}
	return hashes
}

// GetFingerprintType returns the type of fingerprint
func (f *MinHashFingerprinter) GetFingerprintType() string {
	return fmt.Sprintf("minhash-%d-%s", f.numHashes, f.algorithm)
}

// RollingHashFingerprinter implements rolling hash for incremental similarity detection
type RollingHashFingerprinter struct {
	windowSize int
	algorithm  string
}

// NewRollingHashFingerprinter creates a new rolling hash fingerprinter
func NewRollingHashFingerprinter(windowSize int, algorithm string) (*RollingHashFingerprinter, error) {
	if windowSize <= 0 {
		return nil, fmt.Errorf("invalid window size: %d", windowSize)
	}

	return &RollingHashFingerprinter{
		windowSize: windowSize,
		algorithm:  algorithm,
	}, nil
}

// CalculateFingerprint calculates a rolling hash fingerprint
func (f *RollingHashFingerprinter) CalculateFingerprint(data []byte) string {
	if len(data) < f.windowSize {
		// Data too small, use regular hash
		return f.calculateRegularHash(data)
	}

	// Calculate multiple rolling hash values
	numWindows := len(data) - f.windowSize + 1
	if numWindows > 100 { // Limit number of windows for performance
		numWindows = 100
	}

	fingerprints := make([]uint64, numWindows)
	for i := 0; i < numWindows; i++ {
		window := data[i : i+f.windowSize]
		fingerprints[i] = f.calculateRegularHashUint64(window)
	}

	// Combine fingerprints
	var combined uint64
	for _, fp := range fingerprints {
		combined ^= fp
	}

	return fmt.Sprintf("%016x", combined)
}

// calculateRegularHash calculates regular hash for data
func (f *RollingHashFingerprinter) calculateRegularHash(data []byte) string {
	return fmt.Sprintf("%016x", f.calculateRegularHashUint64(data))
}

// calculateRegularHashUint64 calculates regular hash and returns uint64
func (f *RollingHashFingerprinter) calculateRegularHashUint64(data []byte) uint64 {
	var hash []byte
	switch f.algorithm {
	case "sha256":
		h := sha256.Sum256(data)
		hash = h[:8] // Take first 8 bytes
	case "blake3":
		h := blake3.Sum256(data)
		hash = h[:8]
	default:
		h := sha256.Sum256(data)
		hash = h[:8]
	}

	return binary.BigEndian.Uint64(hash)
}

// CalculateSimilarity calculates similarity between two rolling hash fingerprints
func (f *RollingHashFingerprinter) CalculateSimilarity(hash1, hash2 string) float64 {
	// For rolling hash, we can use bit similarity
	var h1, h2 uint64
	fmt.Sscanf(hash1, "%016x", &h1)
	fmt.Sscanf(hash2, "%016x", &h2)

	// Calculate bit similarity
	return f.calculateBitSimilarity(h1, h2)
}

// calculateBitSimilarity calculates bit-level similarity
func (f *RollingHashFingerprinter) calculateBitSimilarity(x, y uint64) float64 {
	xor := x ^ y
	// Count set bits (Hamming weight)
	bits := 0
	for xor != 0 {
		bits += int(xor & 1)
		xor >>= 1
	}

	// Convert to similarity (0 = completely different, 1 = identical)
	return 1.0 - float64(bits)/64.0
}

// GetFingerprintType returns the type of fingerprint
func (f *RollingHashFingerprinter) GetFingerprintType() string {
	return fmt.Sprintf("rollinghash-%d-%s", f.windowSize, f.algorithm)
}

// FingerprinterFactory creates different types of fingerprinting algorithms
type FingerprinterFactory struct{}

// NewFingerprinterFactory creates a new fingerprinter factory
func NewFingerprinterFactory() *FingerprinterFactory {
	return &FingerprinterFactory{}
}

// CreateFingerprinter creates a fingerprinter based on configuration
func (f *FingerprinterFactory) CreateFingerprinter(config FingerprintConfig) (BlockFingerprinter, error) {
	switch config.Type {
	case "hash":
		return NewHashFingerprinter(config.Algorithm)
	case "simhash":
		return NewSimHashFingerprinter(config.Bits, config.Algorithm)
	case "minhash":
		return NewMinHashFingerprinter(config.NumHashes, config.Algorithm)
	case "rollinghash":
		return NewRollingHashFingerprinter(config.WindowSize, config.Algorithm)
	default:
		return nil, fmt.Errorf("unsupported fingerprint type: %s", config.Type)
	}
}

// FingerprintConfig contains configuration for fingerprinting
type FingerprintConfig struct {
	Type       string // "hash", "simhash", "minhash", "rollinghash"
	Algorithm  string // "sha256", "blake3"
	Bits       int    // For SimHash
	NumHashes  int    // For MinHash
	WindowSize int    // For RollingHash
}

// SimilarityMatcher provides similarity matching capabilities
type SimilarityMatcher struct {
	threshold   float64
	fingerprinter BlockFingerprinter
}

// NewSimilarityMatcher creates a new similarity matcher
func NewSimilarityMatcher(threshold float64, fingerprinter BlockFingerprinter) *SimilarityMatcher {
	return &SimilarityMatcher{
		threshold:   threshold,
		fingerprinter: fingerprinter,
	}
}

// IsSimilar checks if two blocks are similar
func (m *SimilarityMatcher) IsSimilar(data1, data2 []byte) bool {
	if len(data1) == 0 || len(data2) == 0 {
		return false
	}

	fp1 := m.fingerprinter.CalculateFingerprint(data1)
	fp2 := m.fingerprinter.CalculateFingerprint(data2)

	similarity := m.fingerprinter.CalculateSimilarity(fp1, fp2)
	return similarity >= m.threshold
}

// GetSimilarityThreshold returns the similarity threshold
func (m *SimilarityMatcher) GetSimilarityThreshold() float64 {
	return m.threshold
}

// SetSimilarityThreshold sets the similarity threshold
func (m *SimilarityMatcher) SetSimilarityThreshold(threshold float64) {
	m.threshold = threshold
}